// Copyright Epic Games, Inc. All Rights Reserved.

#include "zencore/logging.h"

#include <zencore/string.h>
#include <zencore/testing.h>
#include <zencore/thread.h>

ZEN_THIRD_PARTY_INCLUDES_START
#include <spdlog/details/registry.h>
#include <spdlog/sinks/null_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
ZEN_THIRD_PARTY_INCLUDES_END

#if ZEN_PLATFORM_WINDOWS
#	pragma section(".zlog$a", read)
#	pragma section(".zlog$f", read)
#	pragma section(".zlog$m", read)
#	pragma section(".zlog$s", read)
#	pragma section(".zlog$z", read)
#endif

namespace zen {

// We shadow the underlying spdlog default logger, in order to avoid a bunch of overhead
LoggerRef TheDefaultLogger;

}  // namespace zen

namespace zen::logging {

using MemoryBuffer_t = fmt::basic_memory_buffer<char, 250>;

struct LoggingContext
{
	inline LoggingContext();
	inline ~LoggingContext();

	zen::logging::MemoryBuffer_t MessageBuffer;

	inline std::string_view Message() const { return std::string_view(MessageBuffer.data(), MessageBuffer.size()); }
};

LoggingContext::LoggingContext()
{
}

LoggingContext::~LoggingContext()
{
}

//////////////////////////////////////////////////////////////////////////

static inline bool
IsErrorLevel(int LogLevel)
{
	return (LogLevel == zen::logging::level::Err || LogLevel == zen::logging::level::Critical);
};

static_assert(sizeof(spdlog::source_loc) == sizeof(SourceLocation));
static_assert(offsetof(spdlog::source_loc, filename) == offsetof(SourceLocation, filename));
static_assert(offsetof(spdlog::source_loc, line) == offsetof(SourceLocation, line));
static_assert(offsetof(spdlog::source_loc, funcname) == offsetof(SourceLocation, funcname));

void
EmitLogMessage(LoggerRef& Logger, int LogLevel, const std::string_view Message)
{
	const spdlog::level::level_enum InLevel = (spdlog::level::level_enum)LogLevel;
	Logger.SpdLogger->log(InLevel, Message);
	if (IsErrorLevel(LogLevel))
	{
		if (LoggerRef ErrLogger = zen::logging::ErrorLog())
		{
			ErrLogger.SpdLogger->log(InLevel, Message);
		}
	}
}

void
EmitLogMessage(LoggerRef& Logger, int LogLevel, std::string_view Format, fmt::format_args Args)
{
	zen::logging::LoggingContext LogCtx;
	fmt::vformat_to(fmt::appender(LogCtx.MessageBuffer), Format, Args);
	zen::logging::EmitLogMessage(Logger, LogLevel, LogCtx.Message());
}

void
EmitLogMessage(LoggerRef& Logger, const SourceLocation& InLocation, int LogLevel, const std::string_view Message)
{
	const spdlog::source_loc&		Location = *reinterpret_cast<const spdlog::source_loc*>(&InLocation);
	const spdlog::level::level_enum InLevel	 = (spdlog::level::level_enum)LogLevel;
	Logger.SpdLogger->log(Location, InLevel, Message);
	if (IsErrorLevel(LogLevel))
	{
		if (LoggerRef ErrLogger = zen::logging::ErrorLog())
		{
			ErrLogger.SpdLogger->log(Location, InLevel, Message);
		}
	}
}

void
EmitLogMessage(LoggerRef& Logger, const SourceLocation& InLocation, int LogLevel, std::string_view Format, fmt::format_args Args)
{
	zen::logging::LoggingContext LogCtx;
	fmt::vformat_to(fmt::appender(LogCtx.MessageBuffer), Format, Args);
	zen::logging::EmitLogMessage(Logger, InLocation, LogLevel, LogCtx.Message());
}

void
EmitConsoleLogMessage(int LogLevel, const std::string_view Message)
{
	const spdlog::level::level_enum InLevel = (spdlog::level::level_enum)LogLevel;
	ConsoleLog().SpdLogger->log(InLevel, Message);
}

void
EmitConsoleLogMessage(int LogLevel, std::string_view Format, fmt::format_args Args)
{
	zen::logging::LoggingContext LogCtx;
	fmt::vformat_to(fmt::appender(LogCtx.MessageBuffer), Format, Args);
	zen::logging::EmitConsoleLogMessage(LogLevel, LogCtx.Message());
}

}  // namespace zen::logging

namespace zen::logging::level {

spdlog::level::level_enum
to_spdlog_level(LogLevel NewLogLevel)
{
	return static_cast<spdlog::level::level_enum>((int)NewLogLevel);
}

LogLevel
to_logging_level(spdlog::level::level_enum NewLogLevel)
{
	return static_cast<LogLevel>((int)NewLogLevel);
}

constinit std::string_view LevelNames[] = {ZEN_LEVEL_NAME_TRACE,
										   ZEN_LEVEL_NAME_DEBUG,
										   ZEN_LEVEL_NAME_INFO,
										   ZEN_LEVEL_NAME_WARNING,
										   ZEN_LEVEL_NAME_ERROR,
										   ZEN_LEVEL_NAME_CRITICAL,
										   ZEN_LEVEL_NAME_OFF};

level::LogLevel
ParseLogLevelString(std::string_view Name)
{
	for (int Level = 0; Level < level::LogLevelCount; ++Level)
	{
		if (LevelNames[Level] == Name)
			return static_cast<level::LogLevel>(Level);
	}

	if (Name == "warn")
	{
		return level::Warn;
	}

	if (Name == "err")
	{
		return level::Err;
	}

	return level::Off;
}

std::string_view
ToStringView(level::LogLevel Level)
{
	if (int(Level) < level::LogLevelCount)
	{
		return LevelNames[int(Level)];
	}

	return "None";
}

}  // namespace zen::logging::level

//////////////////////////////////////////////////////////////////////////

namespace zen::logging {

RwLock		LogLevelsLock;
std::string LogLevels[level::LogLevelCount];

void
ConfigureLogLevels(level::LogLevel Level, std::string_view Loggers)
{
	RwLock::ExclusiveLockScope _(LogLevelsLock);
	LogLevels[Level] = Loggers;
}

void
RefreshLogLevels(level::LogLevel* DefaultLevel)
{
	spdlog::details::registry::log_levels Levels;

	{
		RwLock::SharedLockScope _(LogLevelsLock);

		for (int i = 0; i < level::LogLevelCount; ++i)
		{
			level::LogLevel CurrentLevel{i};

			std::string_view Spec = LogLevels[i];

			while (!Spec.empty())
			{
				std::string LoggerName;

				if (auto CommaPos = Spec.find_first_of(','); CommaPos != std::string_view::npos)
				{
					LoggerName = Spec.substr(CommaPos + 1);
					Spec.remove_prefix(CommaPos + 1);
				}
				else
				{
					LoggerName = Spec;
					Spec	   = {};
				}

				Levels[LoggerName] = to_spdlog_level(CurrentLevel);
			}
		}
	}

	if (DefaultLevel)
	{
		spdlog::level::level_enum SpdDefaultLevel = to_spdlog_level(*DefaultLevel);
		spdlog::details::registry::instance().set_levels(Levels, &SpdDefaultLevel);
	}
	else
	{
		spdlog::details::registry::instance().set_levels(Levels, nullptr);
	}
}

void
RefreshLogLevels(level::LogLevel DefaultLevel)
{
	RefreshLogLevels(&DefaultLevel);
}

void
RefreshLogLevels()
{
	RefreshLogLevels(nullptr);
}

void
SetLogLevel(level::LogLevel NewLogLevel)
{
	spdlog::set_level(to_spdlog_level(NewLogLevel));
}

level::LogLevel
GetLogLevel()
{
	return level::to_logging_level(spdlog::get_level());
}

LoggerRef
Default()
{
	ZEN_ASSERT(TheDefaultLogger);
	return TheDefaultLogger;
}

void
SetDefault(std::string_view NewDefaultLoggerId)
{
	auto NewDefaultLogger = spdlog::get(std::string(NewDefaultLoggerId));
	ZEN_ASSERT(NewDefaultLogger);

	spdlog::set_default_logger(NewDefaultLogger);
	TheDefaultLogger = LoggerRef(*NewDefaultLogger);
}

LoggerRef TheErrorLogger;

LoggerRef
ErrorLog()
{
	return TheErrorLogger;
}

void
SetErrorLog(std::string_view NewErrorLoggerId)
{
	if (NewErrorLoggerId.empty())
	{
		TheErrorLogger = {};
	}
	else
	{
		auto NewErrorLogger = spdlog::get(std::string(NewErrorLoggerId));

		ZEN_ASSERT(NewErrorLogger);

		TheErrorLogger = LoggerRef(*NewErrorLogger.get());
	}
}

LoggerRef
Get(std::string_view Name)
{
	std::shared_ptr<spdlog::logger> Logger = spdlog::get(std::string(Name));

	if (!Logger)
	{
		Logger = Default().SpdLogger->clone(std::string(Name));
		spdlog::apply_logger_env_levels(Logger);
		spdlog::register_logger(Logger);
	}

	return *Logger;
}

std::once_flag					ConsoleInitFlag;
std::shared_ptr<spdlog::logger> ConLogger;

void
SuppressConsoleLog()
{
	if (ConLogger)
	{
		spdlog::drop("console");
		ConLogger = {};
	}
	ConLogger = spdlog::null_logger_mt("console");
}

LoggerRef
ConsoleLog()
{
	std::call_once(ConsoleInitFlag, [&] {
		if (!ConLogger)
		{
			ConLogger = spdlog::stdout_color_mt("console");
			spdlog::apply_logger_env_levels(ConLogger);

			ConLogger->set_pattern("%v");
		}
	});

	return *ConLogger;
}

void
InitializeLogging()
{
	TheDefaultLogger = *spdlog::default_logger_raw();
}

void
ShutdownLogging()
{
	spdlog::shutdown();
	TheDefaultLogger = {};
}

bool
EnableVTMode()
{
#if ZEN_PLATFORM_WINDOWS
	// Set output mode to handle virtual terminal sequences
	HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
	if (hOut == INVALID_HANDLE_VALUE)
	{
		return false;
	}

	DWORD dwMode = 0;
	if (!GetConsoleMode(hOut, &dwMode))
	{
		return false;
	}

	dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
	if (!SetConsoleMode(hOut, dwMode))
	{
		return false;
	}
#endif

	return true;
}

void
FlushLogging()
{
	spdlog::details::registry::instance().flush_all();
}

}  // namespace zen::logging

namespace zen {

bool
LoggerRef::ShouldLog(int Level) const
{
	return SpdLogger->should_log(static_cast<spdlog::level::level_enum>(Level));
}

void
LoggerRef::SetLogLevel(logging::level::LogLevel NewLogLevel)
{
	SpdLogger->set_level(to_spdlog_level(NewLogLevel));
}

logging::level::LogLevel
LoggerRef::GetLogLevel()
{
	return logging::level::to_logging_level(SpdLogger->level());
}

thread_local ScopedActivityBase* t_ScopeStack = nullptr;

ScopedActivityBase*
GetThreadActivity()
{
	return t_ScopeStack;
}

ScopedActivityBase::ScopedActivityBase() : m_NextScope{t_ScopeStack}
{
	t_ScopeStack = this;
}

ScopedActivityBase::~ScopedActivityBase()
{
	if (t_ScopeStack != this)
	{
		ZEN_ERROR("invalid t_ScopeStack in ~ScopedActivityBase(). Expected {:#x}, found {:#x}", (uintptr_t)this, (uintptr_t)t_ScopeStack);
		return;
	}
	t_ScopeStack = m_NextScope;
}

std::string_view
EmitActivitiesForLogging(StringBuilderBase& OutString)
{
	OutString.Reset();

	for (auto Bread = GetThreadActivity(); Bread; Bread = Bread->GetNext())
	{
		OutString.Append("\n|>|>|> ");
		Bread->Emit(OutString);
	}

	return OutString.ToView();
}

//////////////////////////////////////////////////////////////////////////

ScopedActivityString::~ScopedActivityString()
{
}

void
ScopedActivityString::Emit(StringBuilderBase& Target)
{
	Target.Append(m_Text);
}

#if ZEN_WITH_TESTS

void
logging_forcelink()
{
}

using namespace std::literals;

TEST_CASE("simple.bread")
{
	ExtendableStringBuilder<256> Crumbs;

	auto EmitBreadcrumbs = [&] {
		Crumbs.Reset();

		for (auto Bread = GetThreadActivity(); Bread; Bread = Bread->GetNext())
		{
			Bread->Emit(Crumbs);
		}

		return Crumbs.ToView();
	};

	SUBCASE("single")
	{
		ScopedActivityString _{"hello"};
		EmitBreadcrumbs();

		CHECK_EQ(Crumbs.ToView(), "hello"sv);
	}

	SUBCASE("multi")
	{
		ScopedActivityString $1{"hello"};
		ScopedActivityString $2{"world"};
		ScopedActivityString $3{"amaze"};

		CHECK_EQ(EmitBreadcrumbs(), "amazeworldhello"sv);
	}

	SUBCASE("multi_defer")
	{
		int					 n = 42;
		ScopedActivityString $1{"hello"};
		ScopedActivityString $2{"world"};
		ScopedActivityString $3{"amaze"};
		ScopedLazyActivity	 $4{[&](StringBuilderBase& Out) { Out << "plant"; }};
		{
			ScopedLazyActivity $5{[&](StringBuilderBase& Out) { Out << n; }};
			ZEN_LOG_SCOPE("{}:{}", "abc", "def");
			CHECK_EQ(EmitBreadcrumbs(), "abc:def42plantamazeworldhello"sv);
		}
		CHECK_EQ(EmitBreadcrumbs(), "plantamazeworldhello"sv);
	}
}

#endif

}  // namespace zen
